make a lispをやる
まずはes6実装見ながらコピペしつつ差分読んだ
ちゃんと自分で書かなかったので2周めやろう
いろんな言語で実装済み
jsでやるか ブラウザで動かしたいし
jsのやつを見ると、ES2015以前だったりするので修正する
step0で必要なその他ファイル
table:files
node_readline.js readlineする
printer.js printする
types.js lisp上の型あたり
history機能がついているのだが、最初は要らないので削ってみると読みやすい
node_readlineから削る
make a lisp自体がいろんな言語で実装できるようになってて、jsでもffiで共通のインターフェースになってる
まあこれはそのまま使う
各ステップごとにそこで完成している
$ node step0_repl.js
これでもう、何もせずechoしてくれるやつが動く
トップレベルでmake test^yourimpl^step0をやれば、既存のテストが走ってくれる
step1
https://gyazo.com/588036c528c040baf5a5baaf3e8e9622
step0は文字列返すだけだった
パースしよう
READがパースする
read_str
printのとこに_pr_strが挟まった
printer readerはコピペして目を通した
正規表現でまるっとトークナイズして
素直にオブジェクトに変換
再帰呼び出ししてる
read_atom
code:js
} else if (token.match(/^"(?:\\.|^\\")*"$/)) { return token.slice(1, token.length - 1).replace(/\\(.)/g, function (_, c) {
return c === "n" ? "\n" : c;
});
ここがわからん
defferable (遅延可能)
step2
https://gyazo.com/b8333bb4b63fb5be7419798b096ccaff
evalにenvとして、処理したい中身を渡すのか
code:js
function _EVAL(ast, env) {
//printer.println("EVAL:", printer._pr_str(ast, true));
if (!types._list_Q(ast)) {
return eval_ast(ast, env);
}
if (ast.length === 0) {
return ast;
}
// apply list
var el = eval_ast(ast, env);
return f.apply(f, el.slice(1));
}
function EVAL(ast, env) {
var result = _EVAL(ast, env);
return (typeof result !== "undefined") ? result : null;
}
リストでなければ、astをそのまま評価するが
リストであれば、関数呼び出しとして返す
code:js
// eval
function eval_ast(ast, env) {
if (types._symbol_Q(ast)) {
if (ast in env) {
} else {
throw new Error("'" + ast.value + "' not found");
}
} else if (types._list_Q(ast)) {
return ast.map(function(a) { return EVAL(a, env); });
} else if (types._vector_Q(ast)) {
var v = ast.map(function(a) { return EVAL(a, env); });
v.__isvector__ = true;
return v;
} else if (types._hash_map_Q(ast)) {
var new_hm = {};
for (k in ast) {
}
return new_hm;
} else {
return ast;
}
}
_symbol_Qの名前がよくわからんが、astがシンボルかどうかを返す
シンボルをそのままevalした場合、env[ast]とあるように、環境上にある何らかの中身そのものを返す
リストやベクタだったら、中身をEVALしてから返す
ハッシュマップも同様に中身をEVALする
どれでもなかったらそのまま帰す
環境というものを、単なる文字と関数のセットとみなせば良い?miyamonz.icon
step3 environments
https://gyazo.com/09c1ff55cb4dc5d20a017000f1fd2503
前のステップで加減乗除の関数をenvとして定義したのを考慮すれば、関数定義というのはenvに対する操作であるということは簡単にわかる
let!で新規環境作成
def!で既存の環境を変更
evalのとこ
code:js
// apply list
var a0 = ast0, a1 = ast1, a2 = ast2, a3 = ast3; switch (a0.value) {
case "def!":
var res = EVAL(a2, env);
return env.set(a1, res);
case "let*":
var let_env = new Env(env);
for (var i=0; i < a1.length; i+=2) {
let_env.set(a1i, EVAL(a1i+1, let_env)); }
return EVAL(a2, let_env);
default:
var el = eval_ast(ast, env), f = el0; return f.apply(f, el.slice(1));
}
listの部分
defかletかで変わる
そうでなければ関数呼び出し
def!
環境にsetする
let*
既存のenvから新規作成
(let* (c 2) c) -> 2 2つ目のとこに文字、値のセットを繰り返せる
letによるenvの作成は、既存のものを持ったまま別に作成する感じなのか
こっからesmのやつ見つけたのでそっちに切り替えた
step4
if, fn, do
これらはenvの操作をする特別なform
code:js
case "do":
case "if":
let cond = EVAL(a1, env);
if (cond === null || cond === false) {
return typeof a3 !== "undefined" ? EVAL(a3, env) : null;
} else {
return EVAL(a2, env);
}
case "fn*":
return (...args) => EVAL(a2, new_env(env, a1, args));
do
do以降を評価する。最後の部分を返り値とする
if
最初を評価して、それに基づいてどっちか評価して返す
fn*
他と違い、評価結果を返すのではなく関数を返す
envの操作をしてる
code:lisp
( (fn* (a) a) 7) -> 7
( (fn* (a) (+ a 1)) 10) -> 11
( (fn* (a b) (+ a b)) 2 3) -> 5
なるほど
関数定義はこれでいいのか
envのところで、引数をkeyとして、渡されたやつをvalueとしてsetすればいいのか
tail call optimization 略してTCO
https://gyazo.com/5379a1c433036b90cbd6737cb4814da7
単にtail callsともいうらしい
eval全体をwhileループに囲う
let*
code:js
case "let*":
let let_env = new_env(env);
for (let i = 0; i < a1.length; i += 2) {
env_set(let_env, a1i, EVAL(a1i + 1, let_env)); }
return EVAL(a2, let_env);
code:js
case "let*":
let let_env = new_env(env);
for (let i = 0; i < a1.length; i += 2) {
env_set(let_env, a1i, EVAL(a1i + 1, let_env)); }
env = let_env;
ast = a2;
break; // continue TCO loop
はあーなるほどね
自分自身を呼ぶんじゃなくて、渡したかった引数にsetしてもっかいwhileループに任せればいいと
なんかv8はここらへん、勝手にいい感じにしてくれないのかな?しらんけど
あと関数をenvとかastをラップできるようにしてある
malfunc
step6 file mutation eval
evalをevilという流れなに?
code:diff
+env_set(repl_env, Symbol.for("eval"), (a) => EVAL(a, repl_env));
+env_set(repl_env, Symbol.for("*ARGV*"), []);
// core.mal: defined using language itself
REP("(def! not (fn* (a) (if a false true)))");
+REP(
+ '(def! load-file (fn* (f) (eval (read-string (str "(do " (slurp f) "\nnil)")))))'
+);
+
+if (process.argv.length > 2) {
+ env_set(repl_env, Symbol.for("*ARGV*"), process.argv.slice(3));
+ REP((load-file "${process.argv[2]}"));
+ process.exit(0);
+}
こんだけ
evalとARGV
load-file
read-string
coreで定義してるが、これはReaderで定義したread_str
strを読んでtokenizeする
slurpもcore
nodeだったらreadFileSyncして、ブラウザっぽかったらhttp requestする
code:js
// String functions
function slurp(f) {
if (typeof process !== 'undefined') {
return readFileSync(f, 'utf-8')
} else {
var req = new XMLHttpRequest()
req.open('GET', f, false)
req.send()
if (req.status !== 200) {
_error(Failed to slurp file: ${f})
}
return req.responseText
}
}
slurpをdo で囲って、ひらすらファイルを読んで最後に\n
これをread-stringにわたすので、まるまるファイルを1行で入力したことになる
atomのサポート
clojureのatomにinspiredされてる
atomは任意の型の一つの値への参照
symbolっぽい?miyamonz.icon
参照を変更できるらしい
これでstateを表現する?
typesにAtom classある
code:js
export class Atom {
constructor(val) {
this.val = val;
}
}
といってもこんだけ
coreに各種関数がある
atom
atom?
deref
atm => atm.val
reset!
(atm, a) => atm.val = a
swap!
(atm, f, ...args) => (atm.val = f(atm.val, ...args))
readerにもread_atomとかある
あとderefのaliasとして@が使えるらしい
step7 quoting
eval ループ
code:js
case "quote":
return a1;
case "quasiquote":
ast = quasiquote(a1);
break; // continue TCO loop
quoteは評価せず、そのまま第一引数を返す
quasiquote
code:js
// eval
const is_pair = (x) => Array.isArray(x) && x.length > 0;
const quasiquote = (ast) => {
if (!is_pair(ast)) {
} else if (ast0 === Symbol.for("unquote")) { } else if (is_pair(ast0) && ast00 === Symbol.for("splice-unquote")) { return [Symbol.for("concat"), ast01, quasiquote(ast.slice(1))]; } else {
return [Symbol.for("cons"), quasiquote(ast0), quasiquote(ast.slice(1))]; }
};
再帰呼び出ししてる
code:lisp
(def! lst (quote (2 3))) -> (2 3)
(quasiquote (1 (unquote lst))) -> (1 (2 3))
(quasiquote (1 (splice-unquote lst))) -> (1 2 3)
quoteの意味は分かるのだが、何も指定しなければlistは関数呼び出しという制約に例外を作るから初学者には悪い気がする
quasiquote
ググってみた感じ
quoteしたいけど内側は評価したい!みたいな感じか?
unquoteで埋め込める
その上で定義見たら分かってきた
code:lisp
user> (quasiquote (1 4 ( 5 6 (5 (unquote lst )) ) ))
(1 4 (5 6 (5 (3 5))))
再帰でもちゃんとunquoteできた
だったら最初からquoteの動作これでいいのでは?miyamonz.icon
まあいいや
step8 macro
repl
code:diff
+ case "defmacro!":
+ let func = _clone(EVAL(a2, env));
+ func.ismacro = true;
+ return env_set(env, a1, func);
+ case "macroexpand":
+ return macroexpand(a1, env);
_clone lispのデータをcloneするやつ
defmacroはenv_setの結果を返す
code:js
function macroexpand(ast, env) {
while (_list_Q(ast) && typeof ast0 === "symbol" && ast0 in env) { let f = env_get(env, ast0); if (!f.ismacro) {
break;
}
ast = f(...ast.slice(1));
}
return ast;
}
astがlist かつ第一引数がsymbolで、かつenvに存在する間ループ
マクロでなければbreak
astの残りをmacroに流す
code:lisp
(defmacro! one (fn* () 1))
これだとあまり
code:lisp
(defmacro! unless (fn* (pred a b) `(if ~pred ~b ~a)))
(unless false 7 8)
ほんでeval時にlistなら必ずmacroexpandを呼ぶようになってる
macroじゃなければ何も変化なし
code:lisp
user> (macroexpand (unless true 1 4))
(if true 4 1)
user> (macroexpand (unless false 1 4))
(if false 4 1)
ちゃんと呼べる
macroも結局はismacroが付いてるだけの関数で
code:js
case "def!":
return env_set(env, a1, EVAL(a2, env));
case "defmacro!":
let func = _clone(EVAL(a2, env));
func.ismacro = true;
return env_set(env, a1, func);
何が違うんだろう
cloneしているかどうか
呼び出し側
expandmacro
code:js
let f = env_get(env, ast0); if(!f.ismacro) break;
ast = f(...ast.slice(1));
eval内のlistの評価(defaultのとこ
code:js
// astに (somefn 5 6) がきてる
if (_malfunc_Q(f)) {
env = new_env(f.env, f.params, args);
ast = f.ast;
break; // continue TCO loop
} else {
return f(...args);
}
よくわからん
new_envしてないとかはあるが
macroが何やっているのかはわかるのだが、defとどう違うのかがいまいちつかめてない
def
ただたんに評価した結果を埋め込む
defmacro
リストを受けてリストを返す関数を定義して、macroとして呼び出せるようにする
defmacroでcondを定義してる
code:lisp
(defmacro! cond
(fn*
(& xs)
(if
(> (count xs) 0)
(list
'if
(first xs)
(if
(> (count xs) 1)
(nth xs 1)
(throw "odd number of forms to cond"))
(cons 'cond (rest (rest xs)))))))
step9 try
try*
フォーム末尾のアスタリスクって時々出てくるけどどういう意味だろう
実装側でもtry catchしてやればいい
CとかGOで実装するときってどうやるんだろ
返り値で見るのかな?
code:diff
--- a/step8_macros.js
+++ b/step9_try.js
@@ -87,6 +87,19 @@ const EVAL = (ast, env) => {
return env_set(env, a1, func);
case "macroexpand":
return macroexpand(a1, env);
+ case "try*":
+ try {
+ return EVAL(a1, env);
+ } catch (exc) {
+ if (a2 && a20 === Symbol.for("catch*")) { + if (exc instanceof Error) {
+ exc = exc.message;
+ }
+ return EVAL(a22, new_env(env, [a21], exc)); + } else {
+ throw exc;
+ }
+ }
case "do":
eval_ast(ast.slice(1, -1), env);
@@ -162,7 +175,7 @@ while (true) {
if (exc instanceof Error) {
console.warn(exc.stack);
} else {
- console.warn(Error: ${exc});
+ console.warn(Error: ${pr_str(exc, true)});
}
}
}
throwのcore関数が必要
stepA
Aってなに?